package logic.nodes.collision;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;

import settings.CheatConfig;

import logic.common.game.Game;
import logic.common.team.Team;
import logic.nodes.ModelNode;
import logic.nodes.MovingController;
import logic.nodes.TeamNode;
import logic.nodes.collision.visibility.SightType;
import logic.nodes.collision.visibility.VisibilityChangedEvent;
import logic.nodes.collision.visibility.VisibilityListener;
import logic.nodes.damagable.DamagableNode;
import logic.nodes.lod.blocks.LeafBlock;
import logic.ships.carrier.Carrier;
import logic.ships.frigate.Frigate;
import logic.ships.hunter.Hunter;
import logic.ships.mothership.MotherShip;
import logic.status.CollidableValueManager;
import logic.status.StatusListener;
import logic.status.StatusValueManager;
import logic.weapons.WeaponFire;
import main.InitGame;
import main.listeners.ListenerContainer;
import main.listeners.ListenerKeeper;
import main.listeners.StatusListenerKeeper;
import map.spawnStation.SpawnStation;
import gameStates.absGamesStates.AbsIngameState;
import input.StraightForwardBinding;

import ai.AITools;

import com.jme.bounding.BoundingBox;
import com.jme.bounding.BoundingSphere;
import com.jme.input.controls.GameControl;
import com.jme.math.FastMath;
import com.jme.math.Matrix3f;
import com.jme.math.Vector3f;
import com.jme.scene.Node;

public abstract class CollidableNode extends ModelNode implements ListenerKeeper, StatusListenerKeeper {
	
	private static final long serialVersionUID = 1L;
	
	private boolean isLarge;
	protected DummyType dummyType;
	protected float size;
	protected ImpulseController newestImpulseController;
	protected List<MovingController> movingControllers;
	protected Collection<RotationImpulseController> rotationImpulseControllers;
	protected List<LeafBlock> leafBlocks;
	protected Collection<WeaponFire> collidedWeaponFires;
	protected CollidableValueManager valueManager;
	protected Collection<CollidableNode> currentCollisions;
	
	private HashMap<Team, Boolean> visMap;
	private ListenerContainer visibilityListeners;
	
	private boolean visisbleToPlayer;
	
	public CollidableNode(String path, String title, Node model, AbsIngameState ingameState) {
		this(path, title, model, DummyType.None, ingameState);
	}
	
	public CollidableNode(String path, String title, Node model, DummyType dummyType, AbsIngameState ingameState) {
		super(path, title, model, ingameState);
		this.dummyType = dummyType;
		
		leafBlocks = new ArrayList<LeafBlock>();
		valueManager = new CollidableValueManager();
		
		movingControllers = new ArrayList<MovingController>();
		rotationImpulseControllers = new ArrayList<RotationImpulseController>();
		
		isLarge = this instanceof MotherShip || this instanceof Frigate  
				|| this instanceof Carrier || this instanceof SpawnStation;
		
		currentCollisions = new HashSet<CollidableNode>();
		
		if(!(this instanceof WeaponFire)) collidedWeaponFires = new HashSet<WeaponFire>();
		
		Game game = ingameState.getGame();
		
		visMap = new HashMap<Team, Boolean>();
		Boolean init = Boolean.valueOf(true);
		visMap.put(game.getTeamA(), init);
		visMap.put(game.getTeamB(), init);
		
		visibilityListeners = new ListenerContainer(VisibilityListener.class);
				
		visisbleToPlayer = true;
		
		applyDummyType();
	}
	
	public Collection<CollidableNode> getCurrentCollisions() { return currentCollisions; }
	
	protected void applyDummyType() {
		setIsCollidable(false);
		switch(dummyType) {
			case None: setIsCollidable(true); break;
			case Client: setIsCollidable(true); break;
			case OnlyCollision: setIsCollidable(true); break;
		}
	}
	
	public DummyType getDummyType() { return dummyType; }
	
	public boolean isVisibleToPlayer() {
		return isVisibleTo(ingameState.getPlayer().getTeam());
	}
	
	public void attachModel() { attachChild(getModel()); }
	
	public void detachModel() { detachChild(getModel()); }
	
	public void setVisible(boolean vis, Team team) {
		if(belongsTo(team)) return;
		
		boolean isVisible = isVisibleTo(team);
		if(vis == isVisible) return;
		
		updateVisibility(vis, team);
	}
	
	public void addVisListener(VisibilityListener l) { visibilityListeners.addListener(l); }
	
	public void removeVisListener(VisibilityListener l) { visibilityListeners.removeListener(l); }
	
	protected void updateVisibility(final boolean vis, final Team team) {
		visMap.put(team, Boolean.valueOf(vis));
		
		ingameState.getThreadPool().addPostRunnable(new Runnable() {
			@Override
			public void run() {
				checkPlayerVisibility();
				
				VisibilityChangedEvent event = new VisibilityChangedEvent(team, vis);
				visibilityListeners.callMethod(VisibilityListener.VISIBILITYCHANGED, event);
			}
		});
	}
	
	public void checkPlayerVisibility() {
		boolean visToPlayer = isVisibleToPlayer();
		if(visToPlayer != visisbleToPlayer) {
			visisbleToPlayer = visToPlayer;
			if(visisbleToPlayer) attachModel();
			else detachModel();
		}
	}
	
	public boolean isVisibleTo(Team team) { return visMap.get(team).booleanValue(); }
	
	public boolean belongsTo(Team team) {
		if(this instanceof TeamNode) return ((TeamNode)this).getTeam() == team;
		return false;
	}
	
	public Collection<WeaponFire> getCollidedWeaponFires() { return collidedWeaponFires; }
	
	public List<LeafBlock> getLeafBlocks() {
		if(!isLarge) assert(leafBlocks.size() <= 8);
		return leafBlocks;
	}
	
	@Override
	public boolean removeFromParent() {
		boolean removed = super.removeFromParent();
		if(removed) removeFromLeafBlocks();
		return removed;
	}
	
	@Override
	public void setParent(Node parent) {
		super.setParent(parent);
		if(parent == null) removeFromLeafBlocks();
	}
	
	protected void removeFromLeafBlocks() {
		assert(parent == null);
		for(LeafBlock leafBlock : leafBlocks) {
			leafBlock.removeLeafNode(this);
		}
		leafBlocks.clear();
	}
	
	protected void addMovingController(MovingController controller) {
		movingControllers.add(controller);
	}
	
	protected void removeMovingController(MovingController controller) {
		movingControllers.remove(controller);
	}
	
	public Collection<MovingController> getMovingControllers() { return movingControllers; }
	
	public float getMass() { return 1f; }
	
	public final boolean isLarge() { return isLarge; }
	
	public float getSpeedInDirection(Vector3f dir) {
		Vector3f moveDir = getMoveDirection();
		if(moveDir.equals(Vector3f.ZERO)) return 0f;
		dir.normalizeLocal();
		float angle = dir.angleBetween(moveDir) * FastMath.RAD_TO_DEG;
		float speed = getSpeedOfMovingControllers();
		speed -= speed * angle / 180f;
		return speed;
	}
	
	public float getSpeedOfMovingControllers() {
		if(movingControllers.isEmpty()) return 0f;
		float speed = 0f;
		for(int i = 0; i < movingControllers.size(); i++) {
			MovingController controller = movingControllers.get(i);
			speed += controller.getCurrentSpeed();
		}
		return speed;
	}
	
	public float getCurrentMovingSpeed() {
		Vector3f moveDir = getMoveDirection();
		if(moveDir.equals(Vector3f.ZERO)) return 0f;
		return getSpeedInDirection(moveDir);
	}
	
	public Vector3f getMoveDirection() {
		Vector3f moveDir = Vector3f.ZERO.clone();
		if(movingControllers.isEmpty()) return moveDir;
		for(int i = 0; i < movingControllers.size(); i++) {
			MovingController controller = movingControllers.get(i);
			if(controller == null) continue;
			if(controller.getCurrentSpeed() != 0f) moveDir.addLocal(controller.getMoveDirection());
		}
		return moveDir.normalize();
	}
	
	public Vector3f getUpVector() { return Vector3f.UNIT_Y; }
	
	public SightType getSightType() { return SightType.Normal; }
	
	public boolean testAgainstWeaponFire() { return true; }
	
	public float getSize() {
		if(bound == null) return 0f;
		
		if(size == 0f) {
			if(bound instanceof BoundingSphere) 
				size = ((BoundingSphere)bound).getRadius();
			else if(bound instanceof BoundingBox) {
				BoundingBox box = (BoundingBox)bound;
				size = AITools.getMax(box.xExtent, box.yExtent, box.zExtent) / 2f;
			}
		}
		return size;
	}
	
	public void addImpulseController(ImpulseController controller) {
		addMovingController(controller);
		addController(controller);
	}
	
	public void removeImpulseController(ImpulseController controller) {		
		removeMovingController(controller);
		removeController(controller);
	}
	
	public void addRotationImpulseController(RotationImpulseController controller) {
		rotationImpulseControllers.add(controller);
		addController(controller);
	}
	
	public void removeRotationImpulseController(RotationImpulseController controller) {
		rotationImpulseControllers.remove(controller);
		removeController(controller);
	}
	
	protected void applyDamage(final int damage) {
		if(CheatConfig.get().getEnableCollDamage() && this instanceof DamagableNode) {
			final DamagableNode node = (DamagableNode)this;
			
			if(InitGame.get().isServer()) {
				ingameState.getThreadPool().addPostRunnable(new Runnable() {
					@Override
					public void run() {
						if(!node.isDestroyed()) node.receiveDamage(damage);
					}
				});
			} else if(node instanceof Hunter) {
				InitGame.get().getClientState().handeCollisionDamage(node.getID(), damage);
			}
		}
	}
	
	public void createImpulse(float speed, Vector3f impulseDir) {
		if((this instanceof Carrier || this instanceof Frigate) && !CheatConfig.get().getCollisionBigShips()) return;
		
		GameControl forward = StraightForwardBinding.getGameControl(StraightForwardBinding.forward);
		
		newestImpulseController = new ImpulseController(this, forward, speed, forward, impulseDir);
		addImpulseController(newestImpulseController);
	}
	
	public ImpulseController getNewestImpulseController() { return newestImpulseController; }
	
	protected void createRotation(float impulseSpeed, Vector3f impulseLoc, Vector3f rotationDir) {		
		addRotationImpulseController(new RotationImpulseController(this, rotationDir, impulseLoc, impulseSpeed));
	}
	
	public static Vector3f rotate(Vector3f v, Vector3f r, float angle) {
		angle *= FastMath.DEG_TO_RAD;
		float cos = FastMath.cos(angle);
		float sin = FastMath.sin(angle);
		float minCos = 1 - cos;
		float xq = v.x * v.x;
		float yq = v.y * v.y;
		float zq = v.z * v.z;
		Matrix3f rotMatrix = new Matrix3f(cos + xq * minCos, 
										v.x * v.y * minCos - v.z * sin, 
										v.x * v.z * minCos + v.y * sin, 
										v.y * v.x * minCos + v.z * sin, 
										cos + yq * minCos, 
										v.y * v.z * minCos - v.x * sin, 
										v.z * v.x * minCos - v.y * sin, 
										v.z * v.y * minCos + v.x * sin, 
										cos + zq * minCos);
		return rotMatrix.mult(r);
	}
	
	@Override
	public ListenerContainer getListenerContainer(Class<?> listenerClass) {
		if(listenerClass == VisibilityListener.class) return visibilityListeners;
		return null;
	}
	
	@Override
	public StatusValueManager<?> getListenerContainer(Class<?> listenerClass, float diff) {
		if(listenerClass == StatusListener.class) return valueManager;
		return null;
	}
}